/*
 * ViewImage.c : little widget based on a canvas to display an image of arbitrary size
 * and zoom into or out of it. Key points:
 * - Fast transition using only CPU operations.
 * - High quality anti-aliassing using bi-cubic resize.
 *
 * Written by T.Pierron, Apr 2011, with ideas from Sean Barrett (http://nothings.org/)
 */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <malloc.h>
#include "SIT.h"
#include "graphics.h"
#include "ViewImage.h"


/* Smoother transitions */
static int factors[] = {100, 150, 200, 300, 400, 550, 800, 1100, 1600, 2300, 3200};
static uint8_t mask[] = {0x80, 0x40, 0x20, 0x10, 0x08, 0x04, 0x02, 0x01};

/* Better to remain private */
typedef struct ViewImage_t *     ViewImage;
typedef uint16_t *               DATA16;
typedef uint32_t *               DATA32;

struct ViewImage_t
{
	Image   original;
	Image   curimg;
	APTR    offgc, canvas, ud;
	Image   minimap;
	Rect    src;         /* Part to be extracted from 'bitmap' */
	Rect    dst;         /* Extracted part will be of given size in the vp. */
	Rect    zoom;
	int     cursor[4];   /* Guides within minimap */
	Bool    hasminimap;  /* User want to use minimap */
	Bool    dispmini;    /* Relevant to display it */
	Bool    dozoom;
	int     width,  height;
	int     mouseX, mouseY;
	int     offsetX, offsetY;
	double  fact;
	int     zoomIdx;
	int     waitconf;
	SIT_CallProc cb;     /* Custom paint callback */
};


#define	BITS         8
#define	VALUES       (1 << BITS)
#define	BG_COLOR     RGB(0x88, 0x88, 0x88)
#define	TILESHFT     7
#define	TILE         (1<<TILESHFT)
#define	TILEMASK     (TILE-1)
#define	TILESTRD(w)  ((((w + TILEMASK) >> TILESHFT) + 7) >> 3)
#define	TILESZ(i)    (TILESTRD(i->width)*((i->height+TILEMASK)>>TILESHFT))
#define VIT_UserData (SIT_TagUser + 0x10)

Image ViewImageReduce(Image source, Image ret, Rect * from, Rect * to)
{
	struct Image_t temp;
	Image src;

	if (from == NULL)
	{
		from = alloca(sizeof *from*2); to = from + 1;
		from->x = from->y = to->x = to->y = 0;
		from->width = source->width;
		from->height = source->height;
		to->width = ret->width;
		to->height = ret->height;
	}

	ret->encoder = source->encoder; /* Keep GFX_Premultiplied flag */
	src = source;
	temp = *source;
	temp.bitmap = NULL;
	int wd = to->width;
	int hd = to->height;

	/* First: resize image using a fast method */
	while (wd <= (from->width >> 1) && hd <= (from->height >> 1))
	{
		int i, j, stride = src->stride;
		temp.width  = from->width  >> 1;
		temp.height = from->height >> 1;
		temp.stride = (temp.width * (temp.bpp>>3) + 3) & ~3;

		if (temp.bitmap == NULL)
			temp.bitmap = calloc(temp.stride, temp.height);

		if (src->bpp == 24)
		{
			DATA8 src0, src1, dest;

			for (j = 0; j < temp.height; j ++)
			{
				src0 = src->bitmap + (j*2 + from->y) * stride + from->x * 3;
				src1 = src0 + stride;
				dest = temp.bitmap + j * temp.stride;

				for (i = temp.width; i > 0; i --, dest += 3)
				{
					dest[0] = (src0[0] + src0[3] + src1[0] + src1[3]) >> 2;
					dest[1] = (src0[1] + src0[4] + src1[1] + src1[4]) >> 2;
					dest[2] = (src0[2] + src0[5] + src1[2] + src1[5]) >> 2;
					src0 += 6;
					src1 += 6;
				}
			}
		}
		else
		{
			DATA32 src0, src1, dest = (DATA32) temp.bitmap;

			for (j = 0; j < temp.height; j ++)
			{
				src0 = (DATA32) (src->bitmap + (j*2 + from->y) * stride + from->x * 4);
				src1 = (DATA32) ((DATA8)src0 + stride);
				for (i = temp.stride; i > 0; i -= 4, dest ++)
				{
					*dest = ((src0[0] >> 2) & 0x3f3f3f3f) +
							((src0[1] >> 2) & 0x3f3f3f3f) +
							((src1[0] >> 2) & 0x3f3f3f3f) +
							((src1[1] >> 2) & 0x3f3f3f3f);
					src0 += 2;
					src1 += 2;
				}
			}
		}
		from->x = from->y = 0;
		from->width = temp.width;
		from->height = temp.height;
		src = &temp;
	}
	/* Now we have to resize the image using a factor between 1 and 2 (not including) */
	if (wd < temp.width || hd < temp.height)
	{
		DATA8  span;
		DATA16 error, err;
		DATA32 sum;
		int    x, y, ys, xerr, yerr, surf, chan = src->bpp >> 3, ws, hs, max;

		DATA8 in  = src->bitmap + from->y * src->stride + from->x * chan;
		DATA8 out = ret->bitmap + to->y * ret->stride;
		DATA8 ptr;

		ws    = from->width;
		hs    = from->height;
		span  = malloc(wd * (chan * 4 + 1) + ws * 2 + 32);
		error = (DATA16) (span  + wd);
		sum   = (DATA32) (error + ws);
		surf  = (wd == ws>>1); /* Edge case: horizontal ratio = 1/2 */

		memset(sum, 0, wd * 4 * chan);

		/* error diffusion for horizontal axis */
		for (x = xerr = 0, yerr = VALUES, err = error; x < wd; x ++)
		{
			int pos = ((x + 1) * ws + wd-1) / wd;
			span[x] = pos - xerr - 1; xerr = pos;
			for (y = span[x]; y > 0; *err++ = VALUES, y --);
			*err++ = yerr = ((x + 1) * ws) % wd * VALUES / wd;
			if (yerr == 0 && (span[x] == 0 || surf)) err[-1] = VALUES;
			yerr = VALUES - yerr;
		}
		surf = (unsigned long long) ws * hs * VALUES / (wd * hd);
		max  = surf * 255;

		for (yerr = VALUES, y = ys = 0; y < hd; y ++, out += ret->stride)
		{
			int    yend = (y + 1) * hs / hd, rem[8];
			DATA8  s;
			DATA32 dst;

			ptr = out + to->x * chan;
			if (ys < yend)
			{
				for (x = 0, err = error, s = in, dst = sum; x < wd; x ++)
				{
					int avg;
					switch (span[x]) {
					case 1:
						switch (chan) {
						case 4:  *dst+=(avg=s[chan]*err[1])+(s[0]<<BITS); dst[chan]+=(s[chan]<<BITS)-avg; dst++; s++;
						case 3:  *dst+=(avg=s[chan]*err[1])+(s[0]<<BITS); dst[chan]+=(s[chan]<<BITS)-avg; dst++; s++;
								 *dst+=(avg=s[chan]*err[1])+(s[0]<<BITS); dst[chan]+=(s[chan]<<BITS)-avg; dst++; s++;
						default: *dst+=(avg=s[chan]*err[1])+(s[0]<<BITS); dst[chan]+=(s[chan]<<BITS)-avg; dst++; s++;
						}
						s += chan; err += 2;
						break;
					default:
						switch (chan) {
						case 4:  *dst += (avg = s[0]*err[0]); dst[chan] += (s[0] << BITS) - avg; dst ++; s ++;
						case 3:  *dst += (avg = s[0]*err[0]); dst[chan] += (s[0] << BITS) - avg; dst ++; s ++;
								 *dst += (avg = s[0]*err[0]); dst[chan] += (s[0] << BITS) - avg; dst ++; s ++;
						default: *dst += (avg = s[0]*err[0]); dst[chan] += (s[0] << BITS) - avg; dst ++; s ++;
						}
						err ++;
					}
				}
				in += src->stride;
				ys ++;
			}

			yerr = ((y + 1) * hs) % hd * VALUES / hd;
			memset(rem, 0, sizeof rem);

			if (yerr == 0)
			{
				/* No error diffusion to apply. This branch is less than 1% of use case though. */
				for (x = wd*chan, dst = sum; x > 0; *ptr++ = *dst < max ? *dst / surf : 255, dst ++, x --);
				memset(sum, 0, wd * 4 * chan);
				continue;
			}
			else for (x = 0, err = error, s = in, dst = sum; x < wd; err += span[x]+1, x ++)
			{
				int avg, a, i;
				for (i = 0; i < chan; i ++, s ++, dst ++) {
					if (span[x] == 1) {
						*dst += (avg = (a = s[chan] * err[1] + (s[0] << BITS)) * yerr >> BITS);
						dst[chan] += (s[chan] + s[0]) * yerr - avg;
						rem[i] = (s[chan] << BITS) - (s[chan] + s[0]) * yerr + (s[0] << BITS);
					} else {
						*dst += avg = (a = s[0] * err[0]) * yerr >> BITS;
						dst[chan] += s[0] * yerr - avg;
						rem[i] = (s[0] << BITS) - s[0] * yerr;
					}
					*ptr++ = *dst < max ? *dst / surf : 255;
					*dst = a - avg + rem[4+i];
					rem[i] -= a - avg;
				}
				rem[4] = rem[0]; rem[5] = rem[1]; rem[6] = rem[2]; rem[7] = rem[3];
				s += span[x] * chan;
			}
			in += src->stride;
			ys ++;
		}
		free(span);
	}
	else /* Factor = 1 */
	{
		DATA8 s, d;
		int   chan = ret->bpp>>3;
		for (d = ret->bitmap + to->y * ret->stride + to->x * chan,
		     s = src->bitmap + from->y * src->stride + from->x * chan, chan *= to->width; to->height > 0;
		     d += ret->stride, s += src->stride, to->height --)
			memcpy(d, s, chan);
	}
	if (temp.bitmap) free(temp.bitmap);

	return ret;
}

static void SET_DST(ViewImage v)
{
	v->dst.x = MAX(v->zoom.x, 0);
	v->dst.y = MAX(v->zoom.y, 0);
	v->dst.width  = MIN(v->width,  v->zoom.width);
	v->dst.height = MIN(v->height, v->zoom.height);
	int x = v->zoom.x + v->zoom.width;  if (x > v->width)  x = v->width;
	int y = v->zoom.y + v->zoom.height; if (y > v->height) y = v->height;
	if (v->dst.x + v->dst.width  > x) v->dst.width  = x-v->dst.x;
	if (v->dst.y + v->dst.height > y) v->dst.height = y-v->dst.y;
}

static void SET_BITMAP(Image i, Rect * r, DATA8 tiles, int rop)
{
	int   x1, x2, y1, y2, sz, x;
	DATA8 line = alloca(sz = TILESTRD(i->width));

	memset(line, 0, sz);
	for (x1 = x = r->x >> TILESHFT, x2 = (r->width + TILEMASK) >> TILESHFT; x < x2; x ++)
		line[x>>3] |= mask[x&7];

	y1 = r->y >> TILESHFT, x1 >>= 3, x2 = (x2 + 7) >> 3, tiles += y1*sz;
	for (y2 = (r->height + TILEMASK) >> TILESHFT; y1 < y2; y1 ++, tiles += sz)
		for (x = x1; x < x2; x ++)
			if (rop) tiles[x] &= ~line[x]; else tiles[x] |= line[x];
}

static Bool IterTile(Image i, Rect * from, Rect * sub, DATA8 tiles)
{
	Rect r = {.x = from->x >> TILESHFT, .width  = from->width  >> TILESHFT,
	          .y = from->y >> TILESHFT, .height = from->height >> TILESHFT};

	DATA8 line;
	int   x, x2, y2, sz;

	for (sz = TILESTRD(i->width), tiles += r.y * sz; r.y < r.height; r.y ++, tiles += sz)
		for (x = r.x; x < r.width; x ++)
			if ((tiles[x>>3] & mask[x&7]) == 0) goto break_all;

	return False; /* All tiles processed */
	break_all:
	memset(line = alloca(sz), 0, sz); x2 = x;
	/* Horizontal expand */
	do { line[x2>>3] |= mask[x2&7]; x2 ++; } while(x2 < r.width && (tiles[x2>>3] & mask[x2&7]) == 0);
	/* Vertical expand */
	for (y2 = r.y+1, r.x = x; y2 < r.height; y2 ++) {
		for (tiles += sz, x = 0; x < sz && (tiles[x] & line[x]) == 0; x ++);
		if (x < sz) break;
	}
	sub->x = r.x << TILESHFT; sub->width  = sub->x + ((x2 - r.x) << TILESHFT);
	sub->y = r.y << TILESHFT; sub->height = sub->y + ((y2 - r.y) << TILESHFT);
	return True;
}

static void RENDER_TILE(ViewImage v, Image i, Rect * from)
{
	Rect to;
	if (from->width  > i->width)  from->width  = i->width;
	if (from->height > i->height) from->height = i->height;

	to.x = (from->x * v->zoom.width  + (i->width>>1))  / i->width;
	to.y = (from->y * v->zoom.height + (i->height>>1)) / i->height;

	to.width  = (from->width  * v->zoom.width  + (i->width>>1))  / i->width  - to.x;
	to.height = (from->height * v->zoom.height + (i->height>>1)) / i->height - to.y;
	SET_BITMAP(i, from, (DATA8) (v->curimg+1), 0);
	from->width  -= from->x;
	from->height -= from->y;

	ViewImageReduce(v->original, v->curimg, from, &to);
	memcpy(from, &to, sizeof to);
}

static void SET_LAYER(ViewImage v)
{
	if (v->curimg) GFX_FreeImage(v->curimg), v->curimg = NULL;
	if (v->fact < 1)
	{
		Rect from;
		Image i = v->original;

		/* Lazy rendering: only resize part that is visible (rounded to nearest tile size) */
		from.x = (v->src.x & ~TILEMASK);
		from.y = (v->src.y & ~TILEMASK);
		from.width  = (v->src.x + v->src.width  + TILE - 1) & ~TILEMASK;
		from.height = (v->src.y + v->src.height + TILE - 1) & ~TILEMASK;

		v->curimg = GFX_CreateImageEx(v->zoom.width, v->zoom.height, v->original->bpp, TILESZ(i));
		memset(v->curimg+1, 0, TILESZ(i));
		RENDER_TILE(v, i, &from);
	}
}

static void ViewImageScale(ViewImage v, double f, int x, int y, int rel)
{
	Image i = v->original;

	if (v->width == 0 || v->height == 0) {
		if (f != 1) v->waitconf &= ~2;
		return;
	}

	if (rel)
	{
		v->fact *= f;

		if (f > 1 && v->fact < 1 && v->fact*1.5 > 1)
			v->fact = 1;

		/* Avoid having 100.021345% zoom factor above 100% */
		if (f > 1 && v->fact > 1.1 && v->zoomIdx < DIM(factors))
			v->zoomIdx ++, v->fact = factors[v->zoomIdx] / 100.;

		if (f < 1 && v->zoomIdx > 0)
			v->zoomIdx --, v->fact = factors[v->zoomIdx] / 100.;
	}
	else /* Absolute */
	{
		v->fact = f;
		v->zoomIdx = 0;
		int diff = f * 100, j;
		if (f > 1)
			for (j = 1; j < DIM(factors); j ++)
				if (abs(diff - factors[v->zoomIdx]) > abs(diff - factors[j]))
					v->zoomIdx = j;
	}

	if (v->fact <= 0) v->fact = 1;
	if (v->fact > 32) v->fact = 32;

	/* Image dimension according to zoom level */
	int oldw = v->zoom.width;  if (oldw == 0) oldw = 1;
	int oldh = v->zoom.height; if (oldh == 0) oldh = 1;

	v->zoom.width  = i->width * v->fact + 0.5;
	v->zoom.height = i->height * v->fact + 0.5;
	v->zoom.x      = x - (x-v->zoom.x)*v->zoom.width/oldw;
	v->zoom.y      = y - (y-v->zoom.y)*v->zoom.height/oldh;

	/* Which translate into this rectangle in the viewport */
	SET_DST(v);

	/* What part of the image need to be extracted */
	v->src.x = - MIN(v->zoom.x, 0) * i->width / v->zoom.width;
	v->src.y = - MIN(v->zoom.y, 0) * i->height / v->zoom.height;
	v->src.width = v->dst.width * i->width / v->zoom.width;
	v->src.height = v->dst.height * i->height / v->zoom.height;
	v->dispmini = v->hasminimap && (v->zoom.width > 3*v->width/2 || v->zoom.height > 3*v->height/2);

	if (f == 1 && v->fact < 1 && v->curimg) /* Only window size changed */
	{
		Rect from, sub;

		from.x = (v->src.x & ~TILEMASK);
		from.y = (v->src.y & ~TILEMASK);
		from.width  = (v->src.x + v->src.width  + TILE - 1) & ~TILEMASK;
		from.height = (v->src.y + v->src.height + TILE - 1) & ~TILEMASK;

		while (IterTile(i, &from, &sub, (DATA8) (v->curimg+1)))
			RENDER_TILE(v, i, &sub);
	}
	else /* Window and image size changed */
	{
		SET_LAYER(v);

		v->waitconf &= ~2;
	}
}

static void ViewImageTranslate(ViewImage v, int x, int y)
{
	Image i = v->original;
	v->zoom.x += x - v->mouseX;
	v->zoom.y += y - v->mouseY;
	v->dst.x   = MAX(v->zoom.x, 0);
	v->dst.y   = MAX(v->zoom.y, 0);
	v->src.x   = - MIN(v->zoom.x, 0) * i->width  / v->zoom.width;
	v->src.y   = - MIN(v->zoom.y, 0) * i->height / v->zoom.height;
	v->mouseX  = x;
	v->mouseY  = y;
	SET_DST(v);
	v->src.width  = v->dst.width  * i->width  / v->zoom.width;
	v->src.height = v->dst.height * i->height / v->zoom.height;

	/* Load unrendered tiles */
	if (v->zoom.width < i->width)
	{
		Rect from, sub;

		from.x = (v->src.x & ~TILEMASK);
		from.y = (v->src.y & ~TILEMASK);
		from.width  = (v->src.x + v->src.width  + TILE - 1) & ~TILEMASK;
		from.height = (v->src.y + v->src.height + TILE - 1) & ~TILEMASK;

		while (IterTile(i, &from, &sub, (DATA8) (v->curimg+1)))
			RENDER_TILE(v, i, &sub);
	}
}

/* Check if user clicked within minimap, move vp accordingly if so */
static Bool ViewImageHandleMini(ViewImage v, int x, int y)
{
	if (v->dispmini && v->minimap)
	{
		Image i  = v->minimap;
		x -= v->width - i->width;
		y -= v->height - i->height;
		if (x >= 0 && y > 0)
		{
			/* Click inside, move vp */
			x = - (x - ((v->cursor[2] - v->cursor[0])>>1)) * v->zoom.width  / i->width  - v->zoom.x;
			y = - (y - ((v->cursor[3] - v->cursor[1])>>1)) * v->zoom.height / i->height - v->zoom.y;
			v->mouseX = v->mouseY = 0;
			ViewImageTranslate(v, x, y);
			SIT_Refresh(v->canvas, 0, 0, 0, 0, False);
			return True;
		}
	}
	return False;
}

static int ViewImageMouse(SIT_Widget w, APTR cd, APTR ud)
{
	SIT_OnMouse * msg = cd;
	ViewImage     v   = ud;

	if (v->original == NULL || ! v->dozoom) return 0;
	switch (msg->button) {
	case 3: // Wheel
		ViewImageScale(ud, msg->state < 0 ? 1/1.5 : 1.5, msg->x, msg->y, True);
		SIT_Refresh(w, 0, 0, 0, 0, False);
		break;
	case 0: // LMB
		switch (msg->state) {
		case SITOM_ButtonPressed:
			if (! ViewImageHandleMini(v, msg->x, msg->y))
			{
				v->mouseX = msg->x;
				v->mouseY = msg->y;
				return 1;
			}
			break;
		case SITOM_CaptureMove:
			ViewImageTranslate(v, msg->x, msg->y);
			SIT_Refresh(w, 0, 0, 0, 0, False);
			break;
		}
	}
	return 0;
}

static void ViewImageFullScr(ViewImage v)
{
	Image i = v->original;
	if (i == NULL) return;
	if (i->width < v->width && i->height < v->height)
		v->dst.width = i->width, v->dst.height = i->height;

	if (i->width * v->height > i->height * v->width)
		v->dst.width = v->width, v->dst.height = i->height * v->width / i->width;
	else
		v->dst.height = v->height, v->dst.width = i->width * v->height / i->height;

	v->zoom.width  = v->dst.width;
	v->zoom.height = v->dst.height;
	v->fact  = v->zoom.width / (double) i->width;
	v->dst.x = v->zoom.x = (v->width  - v->dst.width)  / 2;
	v->dst.y = v->zoom.y = (v->height - v->dst.height) / 2;
	v->src.x = v->src.y = 0;
	v->src.width  = i->width;
	v->src.height = i->height;
	v->dispmini = False;
	SET_LAYER(v);
}

int ViewImageKbd(SIT_Widget w, APTR cd, APTR ud)
{
	SIT_OnKey * msg = cd;
	ViewImage   v   = ud;

	v->mouseX = v->mouseY = 0;
	if (v->original == NULL || ! v->dozoom) return 0;

	switch (msg->keycode) {
	case 'f': case 'F': // full-screen
		v->waitconf |= 2;
		ViewImageFullScr(v);
		break;
	case 'd': case 'D': // dot for dot
		ViewImageScale(v, 1, v->width/2, v->height/2, False);
		break;
	case '=':
	case '+': ViewImageScale(v, 1.5,   v->width/2, v->height/2, True); break; // Zoom in
	case '-': ViewImageScale(v, 1/1.5, v->width/2, v->height/2, True); break; // Zoom out
	case SITK_Left:   ViewImageTranslate(v,  10,   0); break;
	case SITK_Right:  ViewImageTranslate(v, -10,   0); break;
	case SITK_Up:     ViewImageTranslate(v,   0,  10); break;
	case SITK_Down:   ViewImageTranslate(v,   0, -10); break;
	default: return 0;
	}
	SIT_Refresh(w, 0, 0, 0, 0, False);
	return 1;
}

static void ViewImageSetMini(ViewImage v, Bool set, Bool refresh)
{
	Image i;
	int   w = 0, h = 0;
	v->hasminimap = set;
	if (set && ! v->minimap && (i = v->original))
	{
		int mw, mh;
		if (i->width > i->height)
			mw = 100, mh = i->height * 100 / i->width;
		else
			mh = 100, mw = i->width * 100 / i->height;
		v->minimap = ViewImageReduce(i, GFX_CreateImage(mw, mh, i->bpp), NULL, NULL);
		GFX_FlattenImage(v->minimap, BG_COLOR);
		memset(v->cursor, 0, sizeof v->cursor);
		w = mw+2; h = mh+2;
		v->dispmini = (v->zoom.width > 3*v->width/2 || v->zoom.height > 3*v->height/2);
	}
	else if (! set && v->minimap)
	{
		i = v->minimap;
		w = i->width+2; h = i->height+2;
		GFX_FreeImage(v->minimap);
		v->minimap = NULL;
	}
	if (refresh && w > 0)
		SIT_Refresh(v->canvas, v->width - w, v->height - h, w, h, False);
}

/* OnResize event handler */
int ViewImageResize(SIT_Widget w, APTR cd, APTR ud)
{
	ViewImage v = ud;

	SIT_GetValues(w, SIT_Width, &v->width, SIT_Height, &v->height, NULL);
	if (v->original == NULL) { v->waitconf &= ~1; return 0; }

	if (v->waitconf)
	{
		if (v->waitconf & 2) ViewImageFullScr(v);
		else ViewImageScale(v, 1, 0, 0, True);
		v->waitconf &= ~1;
	}
	else ViewImageScale(v, 1, 0, 0, True);
	return 1;
}

/* Direct blit using xOR within the image */
void ViewImageDrawCursor(Image img, int * coords)
{
	int i, j, s, e, dir, chan = img->bpp>>3;
	for (i = 0; i < 4; i ++)
	{
		DATA8 p;
		switch (i) {
		case 0: s = coords[0]; e = img->height; dir = 0; break;
		case 1: s = coords[1]; e = img->width;  dir = 1; break;
		case 2: s = coords[2]; e = img->height; dir = 0; break;
		case 3: s = coords[3]; e = img->width;  dir = 1; break;
		default: return;
		}
		if (dir == 0) // Vertical
		{
			if (s < 0 || s >= img->width) continue;
			for (p = img->bitmap + s * chan, j = 0; j < e; p += img->stride, j ++)
				p[0] ^= 0xff, p[1] ^= 0xff, p[2] ^= 0xff;
		} else {
			if (s < 0 || s >= img->height) continue;
			for (p = img->bitmap + s * img->stride, j = 0; j < e; p += chan, j ++)
				p[0] ^= 0xff, p[1] ^= 0xff, p[2] ^= 0xff;
		}
	}
}

int ViewImagePaint(SIT_Widget w, APTR gc, APTR ud)
{
	ViewImage v = ud;

	if (v->original == NULL) return 0;

	if (v->dispmini)
	{
		Image i = v->minimap;
		int   x, y;
		if (i == NULL)
			ViewImageSetMini(v, True, False), i = v->minimap;
		ViewImageDrawCursor(i, v->cursor);
		x = -MIN(v->zoom.x, 0);
		y = -MIN(v->zoom.y, 0);
		v->cursor[0] = x * i->width / v->zoom.width;
		v->cursor[1] = y * i->height / v->zoom.height;
		v->cursor[2] = (x+v->width) * i->width / v->zoom.width;
		v->cursor[3] = (y+v->height) * i->height / v->zoom.height;
		x = v->width-i->width;
		y = v->height-i->height;
		ViewImageDrawCursor(i, v->cursor);
		GFX_SetBgColor(gc, RGB(255,255,255));
		GFX_SetPixels(i, 0, 0, i->width, i->height, gc, x, y, i->width, i->height);
		GFX_FillRect(gc, x-2, y-2, v->width, y-1);
		GFX_FillRect(gc, x-2, y, x-1, v->height);
		GFX_ExcludeClipRect(gc, x-2, y-2, i->width+2, i->height+2);
	}

	GFX_SetBgColor(gc, BG_COLOR);

	if (v->original->bpp == 32)
	{
		#define	ROUND(x)    (((x) + 31) & ~31)

		/* Need to use offscreen gc to avoid flicker */
		Rect r;
		GFX_GetRefresh(v->offgc, &r);
		if (v->offgc == NULL || r.width < ROUND(v->width) || r.height < ROUND(v->height))
		{
			if (v->offgc) GFX_Free(v->offgc);
			v->offgc = GFX_AllocOffScreen(w, ROUND(v->width), ROUND(v->height));
		}
		GFX_SetBgColor(v->offgc, BG_COLOR);
		GFX_FillRect(v->offgc, 0, 0, v->width, v->height);
		if (v->fact < 1)
		{
			if (v->curimg)
				GFX_SetPixels(v->curimg, - MIN(v->zoom.x, 0), - MIN(v->zoom.y, 0), v->dst.width, v->dst.height,
							  v->offgc,  v->dst.x, v->dst.y, v->dst.width, v->dst.height);
			else
				GFX_SetPixels(v->original, v->src.x, v->src.y, v->src.width, v->src.height,
							  v->offgc,    v->dst.x, v->dst.y, v->dst.width, v->dst.height);
		}
		else GFX_SetPixels(v->original, v->src.x, v->src.y, v->src.width, v->src.height,
	              v->offgc, v->dst.x, v->dst.y, v->dst.width, v->dst.height);

		if (v->cb) v->cb(w, v->offgc, v->ud);

	    GFX_CopyGC(v->offgc, 0, 0, v->width, v->height, gc, 0, 0);
	}
	else /* Direct blit */
	{
		if (v->curimg)
			GFX_SetPixels(v->curimg, - MIN(v->zoom.x, 0), - MIN(v->zoom.y, 0), v->dst.width, v->dst.height,
						  gc,  v->dst.x, v->dst.y, v->dst.width, v->dst.height);
		else
			GFX_SetPixels(v->original, v->src.x, v->src.y, v->src.width, v->src.height,
						  gc,    v->dst.x, v->dst.y, v->dst.width, v->dst.height);

		/* Clear unused space */
		int x;
		if (v->dst.x > 0)  GFX_FillRect(gc, 0, 0, v->dst.x-1, v->height);
		if (v->dst.y > 0)  GFX_FillRect(gc, 0, 0, v->width, v->dst.y-1);  x = v->dst.x+v->dst.width-1;
		if (x < v->width)  GFX_FillRect(gc, x, 0, v->width, v->height);   x = v->dst.y+v->dst.height-1;
		if (x < v->height) GFX_FillRect(gc, 0, x, v->width, v->height);

		if (v->cb) v->cb(w, gc, v->ud);
	}

	return 1;
}


int ViewImageFree(SIT_Widget w, APTR cd, APTR ud)
{
	ViewImage v = ud;
	if (v->minimap) GFX_FreeImage(v->minimap);
	if (v->curimg)  GFX_FreeImage(v->curimg);
	if (v->offgc)   GFX_Free(v->offgc);
	free(v);
	return 1;
}

/* Change or set the current image displayed */
void ViewImageSet(ViewImage v, Image i)
{
	v->original = i;

	if (v->curimg) GFX_FreeImage(v->curimg), v->curimg = NULL;
	ViewImageSetMini(v, v->hasminimap, False);

	if ((v->waitconf & 1) == 0) {
		ViewImageResize(v->canvas, NULL, v);
		SIT_Refresh(v->canvas, 0, 0, 0, 0, False);
	}
}

/*
 * Properties : image, factor, allowzoom, autofit
 */
int ViewImageSetGet(SIT_Widget w, APTR cd, APTR ud)
{
	SIT_OnVal * val = cd;
	ViewImage   v   = ud;
	int         ret = 0;

	switch (val->stage) {
	case SITV_Set:
		switch (val->tag) {
		case VIT_Image:     ViewImageSet(ud, SIT_GET(val, APTR)); break;
		case VIT_Factor:    ViewImageScale(ud, SIT_GET(val, double), 0, 0, False); ret = 1; break;
		case VIT_AllowZoom: v->dozoom = SIT_GET(val, int); break;
		case VIT_MiniMap:   ViewImageSetMini(v, SIT_GET(val, int), True); break;
		case VIT_Overlay:   v->cb = SIT_GET(val, APTR); ret = 1; break;
		case VIT_UserData:  v->ud = SIT_GET(val, APTR); break;
		case VIT_AutoFit:
			if (SIT_GET(val, int)) v->waitconf |= 2, ViewImageFullScr(v), ret = 1;
			else v->waitconf &= ~2;
		}
		if (ret && (v->waitconf & 1) == 0) SIT_Refresh(v->canvas, 0, 0, 0, 0, False);
		break;
	case SITV_Get:
		switch (val->tag) {
		case VIT_Image:     SIT_SET(val, v->original, APTR); break;
		case VIT_Factor:    SIT_SET(val, v->fact, double); break;
		case VIT_AllowZoom: SIT_SET(val, v->dozoom, int); break;
		case VIT_MiniMap:   SIT_SET(val, v->hasminimap, int); break;
		case VIT_Overlay:   SIT_SET(val, v->cb, APTR); break;
		case VIT_AutoFit:   SIT_SET(val, v->waitconf & 2, int); break;
		case VIT_UserData:  SIT_SET(val, v->ud, APTR);
		}
	// case SITV_PostProcess:
	}
	return 0;
}

void ViewImageInvalidate(SIT_Widget c, int x, int y, int w, int h)
{
	ViewImage v = NULL;
	Image     i;
	Rect      from;

	SIT_GetValues(c, VIT_UserData, &v, NULL); if (v == NULL) return;
	i = v->original;                          if (i == NULL) return;
	if (h == 0 && w == 0)
		x = y = 0, w = i->width, h = i->height; /* Whole area */

	from.x = x; from.width  = x + w; if (from.width  > i->width)  from.width  = i->width;
	from.y = y; from.height = y + h; if (from.height > i->height) from.height = i->height;

	if (v->curimg)
		SET_BITMAP(i, &from, (DATA8) (v->curimg + 1), 1);

	/* Check if part of invalidated region is visible: intersect rect between 'from' and 'v->src' */
	Rect r = {.x = MAX(from.x, v->src.x), .width  = v->src.x + v->src.width,
			  .y = MAX(from.y, v->src.y), .height = v->src.y + v->src.height};

	if (r.width  > from.width)  r.width  = from.width;
	if (r.height > from.height) r.height = from.height;

	if (r.x < r.width && r.y < r.height)
	{
		/* Round to nearest tile boundary */
		if (v->curimg) {
			r.x &= ~TILEMASK; r.width  = (r.width  + TILEMASK) & ~ TILEMASK;
			r.y &= ~TILEMASK; r.height = (r.height + TILEMASK) & ~ TILEMASK;
			/* Part of invalidated area is visible */
			RENDER_TILE(v, i, &r);
			r.x -= (v->src.x * v->zoom.width  + (i->width>>1)) / i->width;
			r.y -= (v->src.y * v->zoom.height + (i->height>>1)) / i->height;
		}
		else
		{
			r.x = (r.x - v->src.x) * v->zoom.width  / i->width;
			r.y = (r.y - v->src.y) * v->zoom.height / i->height;
			r.width  = (from.width  - v->src.x) * v->zoom.width  / i->width;
			r.height = (from.height - v->src.y) * v->zoom.height / i->height;
		}
		SIT_Refresh(c, r.x + v->dst.x, r.y + v->dst.y, r.width, r.height, False);
	}
	if ((i = v->minimap)) {
		int w = i->width + 2, h = i->height + 2;
		GFX_FreeImage(i); v->minimap = NULL;
		if (v->dispmini) SIT_Refresh(c, v->width - w, v->height - h, w, h, False);
	}
}

Bool ViewImageInit(SIT_Widget w, Image i)
{
	ViewImage v = calloc(sizeof *v, 1);

	if (v == NULL) return False;
	SIT_GetValues(w, SIT_Width, &v->width, SIT_Height, &v->height, NULL);
	v->waitconf = 3;
	v->fact = 1;
	v->hasminimap = v->dozoom = 1;
	v->canvas = w;
	v->ud = v;
	ViewImageSet(v, i);

	SIT_AddCallback(w, SITE_OnClick,      ViewImageMouse,  v);
	SIT_AddCallback(w, SITE_OnMouseMove,  ViewImageMouse,  v);
	SIT_AddCallback(w, SITE_OnRawKey,     ViewImageKbd,    v);
	SIT_AddCallback(w, SITE_OnVanillaKey, ViewImageKbd,    v);
	SIT_AddCallback(w, SITE_OnResize,     ViewImageResize, v);
	SIT_AddCallback(w, SITE_OnPaint,      ViewImagePaint,  v);
	SIT_AddCallback(w, SITE_OnFinalize,   ViewImageFree,   v);
	SIT_AddCallback(w, SITE_OnSetOrGet,   ViewImageSetGet, v);
	SIT_SetFocus(w);
	return True;
}
